Skip to content

fix(*): key a running spawn's live account by its record, not its id alone - #577

Merged
0xKT merged 3 commits into
refactor/ui_web_architecturefrom
fix/spawn_live_key_by_session
Sep 21, 2026
Merged

0xKT merged 3 commits into
refactor/ui_web_architecturefrom
fix/spawn_live_key_by_session

Conversation

@0xKT

@0xKT 0xKT commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Summary

While a spawned sub-agent runs, its live account (transcript, usage, tool calls) is held in a process-wide index. A spawn was keyed into it by the node id the model chose, which is unique for one conversation only, so two conversations running a spawn under the same id shared one entry: the later collecting overwrote it, so the earlier conversation's context panel showed the other run's transcript (and, since #567, its live usage), and the first run to finish popped the entry, so the other run's live view went blank until it ended. This has been possible since a92514e (2026-09-07), when the spawn record id became the model's own node_id; before that the key was a minted, globally unique call id.

The key is now the record's own address, the conversation's node root plus the id (history.spawn_live_key, the spawn side of dag_store.node_live_key). The writer holds the root as the record's directory and the readers as the node files' root, both resolved from the same session directory, so the two sides agree without depending on how a session key is spelled. subagent.context and tasks.list read by the same helper; the dag side already carried its run id. No wire change.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

Run in the fix worktree, base refactor/ui_web_architecture at 9656dd9:

  • uv run pytest tests/test_rpc_tasks.py tests/test_rpc_subagent_calls.py tests/test_subagent_activity.py: 104 passed. Two tests are new: two conversations running a spawn under the same id each read their own usage through tasks.list, and each read their own transcript through subagent.context driven through a real SubagentManager.spawn with a backend that waits to be released, with the first run to finish taking nothing of the other's. The second test was run against the pre-fix source and failed there (conversation A read conversation B's transcript).
  • uv run pytest tests/test_subagent_manager.py: 159 passed.
  • make lint-python and make test-python: 23989 passed, 109 skipped.
  • Relevant Ruff check and format check: passed.
  • Real gateway (raven serve from this branch on the user's home, Raven-Code lane): two conversations dispatched a spawn under the same node_id at once, one saying ALPHA in two steps and one saying BRAVO in five; polled every 3 s, each conversation's subagent.context carried only its own word while both ran, and after the ALPHA run settled the BRAVO conversation's live view stayed up and kept growing (6 to 10 messages) instead of going blank. The fix(*): live spawn record, live usage and a leaner node panel subtitle #567 regression driver on the same gateway still passed its trace and subtitle checks (tool rows 0, 1, 3, 4 while running; answer landed; subtitle Raven-Code / done / 1m02s).
  • Real gateway on an isolated RAVEN_HOME (in-process Raven lane): the tasks.list reader, which now reads by the same key, still served the live usage -- subtitle Raven / running / 5s / 1,881 tokens up to Raven / done / 49s / 8,601 tokens, board chip 1, 2, 3 tools.

Risk

  • Security impact considered: this closes a cross-conversation leak of a running sub-agent's transcript and usage inside one gateway process; nothing new is exposed.
  • Backward compatibility considered: the key is in-memory only, so no record on disk changes shape; a reader and a writer of one process always run the same code.
  • Rollback path is clear for risky changes: revert the squash commit.

Related Issues

Fixes #580. #567 added the second reader of this index (tasks.list) and gated it to running nodes; the collision itself predates it.

0xKT and others added 2 commits September 21, 2026 11:26
The live index of runs in flight is one per process, and a spawn was keyed
into it by the node id the model chose -- unique for one conversation only.
Two conversations running a spawn under the same id therefore shared an
entry: the later one overwrote it, so the earlier conversation's context
panel showed the other run's transcript, and the first to finish popped the
entry, so the other run's live view went blank until it ended. The key is
now the record's own address, the conversation's node root plus the id
(spawn_live_key, the spawn side of node_live_key), which the writer holds as
the record's directory and the readers as the node files' root, both
resolved from the same session directory. subagent.context and tasks.list
read by the same helper.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
The gate on a running node stands on its own -- a settled node has its
account on disk or none -- and the sentence about another conversation's
run under the same id described the key the previous commit replaced.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@0xKT
0xKT requested a review from LivXue as a code owner September 21, 2026 03:37
@0xKT
0xKT requested review from gloryfromca and removed request for LivXue September 21, 2026 03:37

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Reviewed the full target diff and the surrounding spawn writer, live-index readers, session-path derivation, DAG keying, and the history that introduced the live overlay. The writer and both readers derive the same record-address key, while DAG activity remains in its existing namespace and persisted records are unchanged.

I also checked the repository rules in AGENTS.md / CLAUDE.md and the runtime vocabulary and architecture in CONTEXT-MAP.md / CONTEXT.md, backward compatibility, and whether the tests were weakened. Existing assertions were only adapted to the new private key shape; the added tests cover cross-conversation transcript and usage isolation, including one run finishing before the other. No UI/runtime boundary or dependency changes are involved.

Verification: uv run pytest tests/test_rpc_subagent_calls.py tests/test_rpc_tasks.py -x - 90 passed in 10.30s. git diff --check github/refactor/ui_web_architecture...HEAD also passed.

… a wait that gives up

The key changed in one place and its story lived in four: the dag helper's
docstring, the instance index's docstring, the glossary's live-rows entry and
the settled-spawn test's rationale all still said a spawn keys its activity
by a directory name or by an id unique per conversation. All four now state
the record-address rule. The two-conversation test's wait for the other
run to exit also fails loudly on a timeout instead of letting the assertion
after it pass vacuously.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
@0xKT

0xKT commented Sep 21, 2026

Copy link
Copy Markdown
Member Author

Pushed 0426674 on top of the reviewed head: no behaviour change. My own second read found the key's old story still told in four places (the node_live_key docstring, the _live_instances docstring, CONTEXT.md's live-rows entry, and the settled-spawn test's rationale); all four now state the record-address rule. The two-conversation test's wait for the other run to exit also fails loudly on a timeout instead of letting the assertion after it pass vacuously.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

Re-reviewed the full target diff and the delta from the previously reviewed head. The new commit is behavior-neutral: it makes the record-address description consistent across the runtime documentation and code docstrings, and the explicit loop else ensures the concurrency test cannot pass without observing the released run exit.

I rechecked the repository rules and runtime vocabulary, the affected callers and history, backward compatibility, architecture boundaries, and test strength against the unchanged functional diff; none are adversely affected by this follow-up.

Verification on 0426674058c3: uv run pytest tests/test_rpc_subagent_calls.py tests/test_rpc_tasks.py -x - 90 passed in 6.14s. The full target diff also passes git diff --check.

@0xKT

0xKT commented Sep 21, 2026

Copy link
Copy Markdown
Member Author

Real-gateway pass on 0426674, added to the description's Verification:

  • Two conversations dispatched a spawn under the same node_id at once (ALPHA in two steps, BRAVO in five). Polled every 3 s, each conversation's subagent.context carried only its own word while both ran; after the ALPHA run settled, the BRAVO conversation's live view stayed up and kept growing (6 to 10 messages) rather than going blank. On the pre-fix code the same script's first assertion fails (A reads B's transcript).
  • The fix(*): live spawn record, live usage and a leaner node panel subtitle #567 regression driver on the same gateway still passes its trace and subtitle checks (Raven-Code lane), and on an isolated home with the in-process lane the tasks.list reader still serves live usage through the new key (Raven / running / 5s / 1,881 tokens to Raven / done / 49s / 8,601 tokens).

@0xKT
0xKT merged commit 6451e7c into refactor/ui_web_architecture Sep 21, 2026
27 checks passed
@0xKT
0xKT deleted the fix/spawn_live_key_by_session branch September 21, 2026 05:55
gloryfromca pushed a commit that referenced this pull request Sep 21, 2026
…alone (#577)

## Summary

While a spawned sub-agent runs, its live account (transcript, usage,
tool calls) is held in a process-wide index. A spawn was keyed into it
by the node id the model chose, which is unique for one conversation
only, so two conversations running a spawn under the same id shared one
entry: the later `collecting` overwrote it, so the earlier
conversation's context panel showed the other run's transcript (and,
since #567, its live usage), and the first run to finish popped the
entry, so the other run's live view went blank until it ended. This has
been possible since a92514e (2026-09-07), when the spawn record id
became the model's own `node_id`; before that the key was a minted,
globally unique call id.

The key is now the record's own address, the conversation's node root
plus the id (`history.spawn_live_key`, the spawn side of
`dag_store.node_live_key`). The writer holds the root as the record's
directory and the readers as the node files' root, both resolved from
the same session directory, so the two sides agree without depending on
how a session key is spelled. `subagent.context` and `tasks.list` read
by the same helper; the dag side already carried its run id. No wire
change.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run in the fix worktree, base `refactor/ui_web_architecture` at
9656dd9:

- `uv run pytest tests/test_rpc_tasks.py
tests/test_rpc_subagent_calls.py tests/test_subagent_activity.py`: 104
passed. Two tests are new: two conversations running a spawn under the
same id each read their own usage through `tasks.list`, and each read
their own transcript through `subagent.context` driven through a real
`SubagentManager.spawn` with a backend that waits to be released, with
the first run to finish taking nothing of the other's. The second test
was run against the pre-fix source and failed there (conversation A read
conversation B's transcript).
- `uv run pytest tests/test_subagent_manager.py`: 159 passed.
- `make lint-python` and `make test-python`: 23989 passed, 109 skipped.
- Relevant Ruff check and format check: passed.
- Real gateway (`raven serve` from this branch on the user's home,
Raven-Code lane): two conversations dispatched a spawn under the same
`node_id` at once, one saying ALPHA in two steps and one saying BRAVO in
five; polled every 3 s, each conversation's `subagent.context` carried
only its own word while both ran, and after the ALPHA run settled the
BRAVO conversation's live view stayed up and kept growing (6 to 10
messages) instead of going blank. The #567 regression driver on the same
gateway still passed its trace and subtitle checks (tool rows 0, 1, 3, 4
while running; answer landed; subtitle `Raven-Code / done / 1m02s`).
- Real gateway on an isolated `RAVEN_HOME` (in-process `Raven` lane):
the `tasks.list` reader, which now reads by the same key, still served
the live usage -- subtitle `Raven / running / 5s / 1,881 tokens` up to
`Raven / done / 49s / 8,601 tokens`, board chip 1, 2, 3 tools.

## Risk

- [x] Security impact considered: this closes a cross-conversation leak
of a running sub-agent's transcript and usage inside one gateway
process; nothing new is exposed.
- [x] Backward compatibility considered: the key is in-memory only, so
no record on disk changes shape; a reader and a writer of one process
always run the same code.
- [x] Rollback path is clear for risky changes: revert the squash
commit.

## Related Issues

Fixes #580. #567 added the second reader of this index (`tasks.list`)
and gated it to running nodes; the collision itself predates it.

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants